import 'intersection-observer';
import { MutableRefObject, useCallback, useEffect, useState } from 'react';

const isFunction = (value: unknown): boolean => typeof value === 'function';

type TargetType = HTMLElement | Element | Window | Document;
type TargetValue<T> = T | undefined | null;
export type BasicTarget<T extends TargetType = Element> =
  | (() => TargetValue<T>)
  | TargetValue<T>
  | MutableRefObject<TargetValue<T>>;

export const getTargetElement = <T extends TargetType>(
  target: BasicTarget<T>,
) => {
  if (!target) {
    return null;
  }

  let targetElement: TargetValue<T>;
  if (isFunction(target)) {
    targetElement = (target as () => TargetValue<T>)();
  } else if ('current' in target) {
    targetElement = target.current;
  } else {
    targetElement = target as TargetValue<T>;
  }

  return targetElement;
};

export type IntersectionOptions = {
  /** 根 (root) 元素的外边距
   * 比如 "10px 20px 30px 40px" (top, right, bottom, left)
   */
  rootMargin?: string;
  /** target 元素和 root 元素相交程度达到该值的时候
   * IntersectionObserver 注册的回调函数将会被执行
   * 0 - 1
   */
  threshold?: number | number[];
  /** 指定根 (root) 元素，用于检查目标的可见性
   * 必须是目标元素的父级元素
   * 如果未指定或者为null，则默认为浏览器视窗
   */
  root?: Element | Document | null | undefined;
  /** 监听元素 */
  element: Element;
};

const useIntersectionObserver = (): [
  IntersectionObserverEntry | null,
  (P: IntersectionOptions) => void,
] => {
  const [observerEntry, setObserverEntry] =
    useState<null | IntersectionObserverEntry>(null);

  const [observe, setObserver] = useState<null | IntersectionObserver>(null);

  let handle: any = useCallback((entries: any) => {
    for (const entry of entries) {
      setObserverEntry(entry);
    }
  }, []);

  const listen = useCallback(
    ({ rootMargin, threshold, root, element }: IntersectionOptions) => {
      const options: Partial<IntersectionOptions> = {
        rootMargin: rootMargin,
        root: getTargetElement(root),
        threshold: threshold,
      };
      const observer = new IntersectionObserver((entries) => {
        handle?.(entries);
      }, options);
      if (element) {
        observer.observe(element);
        setObserver(observer);
      }
    },
    [],
  );

  useEffect(() => {
    return () => {
      observe?.disconnect?.();
      handle = null;
    };
  }, []);

  return [observerEntry, listen];
};

export { useIntersectionObserver };
